﻿// Nova.Studio - a GUI test framework for the Nova.CodeDOM C# object model library.
// Copyright (C) 2007-2012 Inevitable Software, all rights reserved.
// Released under the Common Development and Distribution License, CDDL-1.0: http://opensource.org/licenses/cddl1.php

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using Microsoft.Win32;

using Nova.CodeDOM;
using Nova.Test;
using Nova.UI;
using Nova.Utilities;

namespace Nova.Studio
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        #region /* FIELDS */

        // True if busy (loading, parsing, etc)
        protected bool _busy;

        protected Stopwatch _busyStopWatch = new Stopwatch();

        // Status bar related
        protected readonly DispatcherTimer _statusTimer;  // 4Hz status update timer
        protected int _heapUsage;
        protected int _maxHeapUsage;

        protected double _tabControlMinHeight;

        #endregion

        #region /* CONSTRUCTORS */

        public MainWindow()
        {
            InitializeComponent();

            // Setup callback for logging class to display logs in the UI
            Log.SetLogWriteLineCallback(AddLogEntry);

            // Force a reference to CodeObject, so that any .config settings are read
            CodeObject.ForceReference();

            // Enumerate system fonts
            foreach (FontFamily font in Fonts.SystemFontFamilies)
                comboBoxFonts.Items.Add(font.Source);
            comboBoxFonts.SelectedItem = CodeRenderer.CodeFontFamily.Source;
            textBoxFontSize.Text = CodeRenderer.CodeFontSize.ToString();

            // Command initialization
            BindCommands(this);
            InputBindings.Add(new InputBinding(ApplicationCommands.Close, new KeyGesture(Key.F4, ModifierKeys.Control)));
            foreach (Type type in Assembly.GetAssembly(typeof(CodeObjectVM)).GetTypes())
            {
                MethodInfo method = type.GetMethod("BindCommands", BindingFlags.Public | BindingFlags.Static);
                if (method != null)
                    method.Invoke(null, new object[] { this });
            }

            // Setup callbacks
            CodeRenderer.ReRenderAll = ReRenderCurrentTab;

            // Setup status update timer at 4Hz
            _statusTimer = new DispatcherTimer(new TimeSpan(0, 0, 0, 0, 250), DispatcherPriority.Normal, OnUpdateStatus, Dispatcher.CurrentDispatcher);
            _statusTimer.Start();

            Log.WriteLine("MainWindow initialization complete.");
        }

        protected void Shutdown()
        {
            _statusTimer.Stop();
            Application.Current.Shutdown();
        }

        #endregion

        #region /* PROPERTIES */

        public bool Busy
        {
            get { return _busy; }
            set
            {
                _busy = value;

                // Force the CanExecute status of all commands to update without having to click
                // somewhere in the UI first (so toolbar buttons update immediately).
                CommandManager.InvalidateRequerySuggested();
            }
        }

        #endregion

        #region /* MAIN MENU & TOOL BARS */

        protected void CanAlwaysExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = !_busy;
        }

        // Keep the open file dialog around so that it remembers the last directory used
        protected OpenFileDialog _openFileDialog;

        protected void OnFileOpen(object sender, ExecutedRoutedEventArgs e)
        {
            if (_openFileDialog == null)
            {
                _openFileDialog = new OpenFileDialog
                                      {
                                          Filter = "C# Files (*.cs)|*.cs|All Files (*.*)|*.*"
                                      };
            }
            _openFileDialog.FileName = null;
            if (_openFileDialog.ShowDialog() == true)
            {
                string fileName = _openFileDialog.FileName;
                switch (Path.GetExtension(fileName))
                {
                    default:
                        OpenIndividualFile(fileName);
                        break;
                }
            }
        }

        protected void CanCloseAllExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = (treeViewFiles.Items.Count > 0 && !_busy);
        }

        protected void OnCloseAll(object sender, ExecutedRoutedEventArgs e)
        {
            CloseAll();
        }

        protected void CloseAll()
        {
            // Remove all tabs (including any that aren't actually linked to the file tree, but that's OK)
            tabControlFiles.Items.Clear();

            // Clear the file tree and other controls
            WPFUtil.Clear(treeViewFiles);
            WPFUtil.Clear(listViewOutput);
            tabItemOutput.IsSelected = true;

            OnUpdateStatus(null, null);
        }

        protected void OpenFileCompleted()
        {
            treeViewFiles.InvalidateVisual();
            ShowProgressBar(null);
            OnUpdateStatus(null, null);
            Busy = false;
        }

        protected void LoadStatusUpdate(LoadStatus loadStatus, CodeObject codeObject)
        {
            switch (loadStatus)
            {
                case LoadStatus.ObjectCreated:
                    treeViewFiles.Dispatcher.Invoke(new Action<CodeObject>(AddItem), codeObject);
                    break;
                case LoadStatus.ObjectAnnotated:
                    treeViewFiles.Dispatcher.Invoke(new Action<CodeObject>(UpdateItem), codeObject);
                    break;
                default:
                    progressBarStatus.Dispatcher.Invoke(new Action<string>(ShowProgressBar), loadStatus.ToString());
                    break;
            }
        }

        protected void OpenIndividualFile(string fileName)
        {
            // First, search for the file in the tree view, and open it if found
            TreeViewItem fileItem = FindFileNameInTree(treeViewFiles.Items, fileName);
            if (fileItem != null)
                SelectFileItemAndOpenTab(fileItem);
            else
            {
                // Add the file to the miscellaneous files
                fileItem = AddMiscellaneousFile(fileName);
                SelectTreeItem(fileItem);
                CodeUnit codeUnit = ((CodeUnitVM)fileItem.Tag).CodeUnit;
                ParseCodeUnit(codeUnit);
            }
        }

        protected void ParseCodeUnit(CodeUnit codeUnit)
        {
            Busy = true;
            WPFUtil.ExecuteActionOnThread(this, ParseCodeUnitThread, codeUnit, ParseCodeUnitCompleted);
        }

        protected void ParseCodeUnitThread(CodeUnit codeUnit)
        {
            codeUnit.ParseLog(LoadOptions.Complete, LoadStatusUpdate);
        }

        protected void ParseCodeUnitCompleted()
        {
            OpenFileCompleted();
            OpenItem((TreeViewItem)treeViewFiles.SelectedItem);
        }

        protected TabItem OpenCodeUnit(CodeUnit codeUnit)
        {
            TabItem tabItem = null;
            TreeViewItem fileItem = FindCodeObjectInTree(treeViewFiles.Items, codeUnit);
            if (fileItem != null)
                tabItem = SelectFileItemAndOpenTab(fileItem);
            return tabItem;
        }

        protected TabItem SelectFileItemAndOpenTab(TreeViewItem fileItem)
        {
            SelectTreeItem(fileItem);

            // If a tab already exists, give it focus, otherwise open the file
            foreach (TabItem tabItem in tabControlFiles.Items)
            {
                if (((TabInfo)tabItem.Tag).TreeViewItem == fileItem)
                {
                    tabItem.IsSelected = true;
                    return tabItem;
                }
            }
            return OpenItem(fileItem);
        }

        protected void CanCloseExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            TabItem tabItem = e.Parameter as TabItem;
            if (tabItem != null)
            {
                TabControl tabControl = tabItem.Parent as TabControl;
                e.CanExecute = (tabControl == tabControlFiles && tabControlFiles != null);
            }
            else
            {
                // Called from Menu
                if (tabControlFiles.SelectedIndex >= 0)
                    e.CanExecute = (tabControlFiles != null && tabControlFiles.SelectedItem != null && !_busy);
            }
        }

        protected void OnFileClose(object sender, ExecutedRoutedEventArgs e)
        {
            TabItem tabItem = e.Parameter as TabItem;
            if (tabItem != null)
            {
                TabControl tabControl = tabItem.Parent as TabControl;
                if (tabControl != null)
                    tabControl.Items.Remove(tabItem);
            }
            else
            {
                // If we had no parameter, close the selected code window (closing from Menu)
                if (tabControlFiles.SelectedIndex >= 0)
                    tabControlFiles.Items.RemoveAt(tabControlFiles.SelectedIndex);
            }
        }

        protected void CanSaveExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = (tabControlFiles != null && tabControlFiles.SelectedItem != null && !_busy);
        }

        protected void CanSaveAllExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = (treeViewFiles.Items.Count > 0 && !_busy);
        }

        protected void OnFileSave(object sender, ExecutedRoutedEventArgs e)
        {
            SaveTabItem((TabItem)tabControlFiles.SelectedItem);
        }

        protected void OnFileSaveAs(object sender, ExecutedRoutedEventArgs e)
        {
            TabItem tabItem = (TabItem)tabControlFiles.SelectedItem;
            CodeObject codeObject = ((TabInfo)tabItem.Tag).CodeVM.CodeObject;
            if (codeObject is CodeUnit)
            {
                CodeUnit codeUnit = (CodeUnit)codeObject;
                SaveFileDialog saveFileDialog = new SaveFileDialog { FileName = CodeUnit.GetSaveFileName(codeUnit.FileName) };
                if (saveFileDialog.ShowDialog() == true)
                {
                    string fileName = saveFileDialog.FileName;
                    codeUnit.SaveAs(fileName);
                }
            }
        }

        protected void OnFileSaveAll(object sender, ExecutedRoutedEventArgs e)
        {
            // Save all open tabs
            foreach (TabItem tabItem in tabControlFiles.Items)
                SaveTabItem(tabItem);
        }

        protected void SaveTabItem(TabItem tabItem)
        {
            CodeObject codeObject = ((TabInfo)tabItem.Tag).CodeVM.CodeObject;
            if (codeObject is CodeUnit)
                ((CodeUnit)codeObject).Save();
        }

        protected void Exit_Click(object sender, RoutedEventArgs e)
        {
            Shutdown();
        }

        protected void OnAbout(object sender, ExecutedRoutedEventArgs e)
        {
            AboutWindow aboutWindow = new AboutWindow { Owner = this };
            aboutWindow.ShowDialog();
        }

        /// <summary>
        /// Get the <see cref="CodeUnit"/> of the active tab or tree selection.
        /// </summary>
        public CodeUnit GetCurrentCodeUnit()
        {
            CodeUnit codeUnit = null;
            TabItem tabItem = tabControlFiles.SelectedItem as TabItem;
            if (tabItem != null)
            {
                CodeUnitVM codeUnitVM = ((TabInfo)tabItem.Tag).CodeVM as CodeUnitVM;
                if (codeUnitVM != null)
                    codeUnit = codeUnitVM.CodeUnit;
                if (codeUnit == null)
                {
                    TreeViewItem treeViewItem = treeViewFiles.SelectedItem as TreeViewItem;
                    if (treeViewItem != null && treeViewItem.Tag is CodeUnitVM)
                        codeUnit = ((CodeUnitVM)treeViewItem.Tag).CodeUnit;
                }
            }
            return codeUnit;
        }

        #endregion

        #region /* MAIN TAB CONTROL */

        protected void tabControlFiles_Loaded(object sender, RoutedEventArgs e)
        {
            App app = (App)Application.Current;
            if (app.Arguments.Length > 0)
            {
                switch (app.Arguments[0])
                {
                    case "manualtests":
                    {
                        // Run simple manual tests
                        AddTestTab("Simple", ManualTests.GenerateSimpleTest);
                        AddTestTab("Method", ManualTests.GenerateMethodTest);
                        AddTestTab("FullTest", ManualTests.GenerateFullTest, true);
                        break;
                    }
                    case "fulltest":
                    {
                        string testProject = FileUtil.RemoveLastDirectory(FileUtil.GetBaseDirectory()) + @"\Nova.Test\";
                        OpenIndividualFile(FileUtil.CombineAndNormalizePath(testProject, "FullTest.cs"));
                        break;
                    }
                }
            }
        }

        private void AddTestTab(string testName, Func<string, CodeObject> generateTest, bool parse)
        {
            // Manually generate code
            CodeObject code = null;
            try
            {
                code = generateTest(testName);
            }
            catch (Exception ex)
            {
                Log.Exception(ex, "generating test '" + testName + "'");
            }

            // Add the code under a new tab in the tab control
            AddMiscellaneousCode(testName, code, true);

            // Create a test tab with a re-parsed version of FullTest
            if (parse && code != null)
            {
                testName += "-Parsed";
                CodeUnit codeUnit = new CodeUnit(testName, code.AsText());
                ParseItem(codeUnit);
                AddMiscellaneousCode(testName, codeUnit, true);
            }
        }

        private void AddTestTab(string testName, Func<string, CodeObject> generateTest)
        {
            AddTestTab(testName, generateTest, false);
        }

        protected TabItem AddTab(string name, CodeObjectVM codeObjectVM, TreeViewItem treeViewItem)
        {
            RemoveTab(codeObjectVM);
            string description = (codeObjectVM is CodeUnitVM ? ((CodeUnitVM)codeObjectVM).CodeUnit.FileName : name);
            // Use a TextBlock for the Header to prevent special handling of any underscores
            TabItem tabItem = new TabItem { Header = new TextBlock { Text = name, ToolTip = description }, Tag = new TabInfo(codeObjectVM, treeViewItem) };
            RenderTab(tabItem);
            tabControlFiles.Items.Insert(0, tabItem);
            tabControlFiles.SelectedIndex = 0;
            return tabItem;
        }

        protected void RemoveTab(CodeObjectVM codeObjectVM)
        {
            foreach (TabItem tabItem in tabControlFiles.Items)
            {
                if (((TabInfo)tabItem.Tag).CodeVM == codeObjectVM)
                {
                    tabControlFiles.Items.Remove(tabItem);
                    break;
                }
            }
        }

        protected void RenderTab(TabItem tabItem)
        {
            if (tabItem != null)
            {
                CodeObjectVM codeVM = ((TabInfo)tabItem.Tag).CodeVM;
                CodeWindow codeWindow = new CodeWindow(((TextBlock)tabItem.Header).Text, codeVM);
                if (_tabControlMinHeight == 0)
                {
                    tabControlFiles.Measure(new Size(double.MaxValue, double.MaxValue));
                    _tabControlMinHeight = tabControlFiles.DesiredSize.Height - 4;  // Approx size of client area
                }
                codeWindow.Render(tabControlFiles.ActualHeight - _tabControlMinHeight);
                tabItem.Content = codeWindow;
            }
        }

        protected void ReRenderCurrentTab()
        {
            RenderTab((TabItem)tabControlFiles.SelectedItem);
        }

        protected void tabControlFiles_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            TabItem tabItem = (TabItem)tabControlFiles.SelectedItem;
            if (tabItem != null)
            {
                TreeViewItem treeViewItem = ((TabInfo)tabItem.Tag).TreeViewItem;
                if (treeViewItem != null)
                    SelectTreeItem(treeViewItem);
            }
        }

        protected void ResetTabToolTipTimer()
        {
            TabItem tabItem = (TabItem)tabControlFiles.SelectedItem;
            if (tabItem != null)
                ((CodeWindow)tabItem.Content).ResetToolTipTimer();
        }

        protected void CloseTabToolTip()
        {
            TabItem tabItem = (TabItem)tabControlFiles.SelectedItem;
            if (tabItem != null)
                ((CodeWindow)tabItem.Content).CloseToolTip();
        }

        public CodeWindow GetCurrentCodeWindow()
        {
            TabItem tabItem = (TabItem)tabControlFiles.SelectedItem;
            return (tabItem != null ? (CodeWindow)tabItem.Content : null);
        }

        #region /* TAB INFO CLASS */

        public class TabInfo
        {
            public CodeObjectVM CodeVM;
            public TreeViewItem TreeViewItem;

            public TabInfo(CodeObjectVM codeVM, TreeViewItem treeViewItem)
            {
                CodeVM = codeVM;
                TreeViewItem = treeViewItem;
            }
        }

        #endregion

        #endregion

        #region /* FILE TREE */

        protected void AddItem(CodeObject codeObject)
        {
            if (codeObject is CodeUnit)
            {
                CodeUnit codeUnit = (CodeUnit)codeObject;
                AddFileItem(treeViewFiles, Path.GetDirectoryName(codeUnit.FileName), codeUnit);
            }
        }

        protected void UpdateItem(CodeObject codeObject)
        {
            if (codeObject is CodeUnit)
            {
                CodeUnit codeUnit = (CodeUnit)codeObject;
                UpdateTreeItem(treeViewFiles, Path.GetDirectoryName(codeUnit.FileName), codeUnit);
            }
        }

        protected static void AddFileItem(ItemsControl itemsControl, string rootPath, CodeUnit codeUnit)
        {
            string relativePath = Path.GetDirectoryName(codeUnit.FileName) + @"\";
            if (relativePath.StartsWith(rootPath))
                relativePath = relativePath.Substring(rootPath.Length);

            // If there's a subdirectory, call this routine recursively, otherwise add the file
            if (!string.IsNullOrEmpty(relativePath))
            {
                string folderName = relativePath.Substring(0, relativePath.IndexOf('\\'));
                string newRoot = rootPath + folderName + @"\";
                int index;
                TreeViewItem folderItem = FindTreeItem(itemsControl, folderName, null, out index);
                if (folderItem == null)
                    folderItem = InsertTreeItem(itemsControl, index, folderName, null, newRoot);
                AddFileItem(folderItem, newRoot, codeUnit);
            }
            else
                AddTreeItem(itemsControl, codeUnit.Name, codeUnit);
        }

        public TreeViewItem AddMiscellaneousFile(string fileName)
        {
            // Create a CodeUnit for the file, and add it to the tree
            CodeUnit codeUnit = new CodeUnit(fileName);
            return AddTreeItem(treeViewFiles, codeUnit.Name, codeUnit);
        }

        public TreeViewItem AddMiscellaneousCode(string name, CodeObject codeObject, bool open)
        {
            // Add the in-memory code fragment to the tree
            TreeViewItem treeViewItem = AddTreeItem(treeViewFiles, name, codeObject);
            if (open)
            {
                SelectTreeItem(treeViewItem);
                OpenItem(treeViewItem);
            }
            return treeViewItem;
        }

        protected static TreeViewItem AddTreeItem(ItemsControl itemsControl, string name, CodeObject codeObject)
        {
            // Use dummy empty-string tooltips for CodeObjects - they will be generated when opened
            string toolTip = codeObject != null ? "" : null;

            int index;
            FindTreeItem(itemsControl, name, codeObject, out index);
            return InsertTreeItem(itemsControl, index, name, codeObject, toolTip);
        }

        protected static TreeViewItem InsertTreeItem(ItemsControl itemsControl, int index, string name, CodeObject codeObject, object toolTip)
        {
            TreeViewItem treeViewItem = new TreeViewItem { Header = name, ToolTip = toolTip, Tag = CodeObjectVM.CreateVM(codeObject, null) };
            treeViewItem.ToolTipOpening += treeViewFiles_ToolTipOpening;
            itemsControl.Items.Insert(index, treeViewItem);
            SetTreeItemColorBasedOnMessages(treeViewItem);
            return treeViewItem;
        }

        protected static void UpdateTreeItem(ItemsControl itemsControl, string name, CodeObject codeObject)
        {
            int index;
            TreeViewItem treeViewItem = FindTreeItem(itemsControl, name, codeObject, out index);
            if (treeViewItem != null)
            {
                SetTreeItemColorBasedOnMessages(treeViewItem);
                CodeObjectVM codeObjectVM = treeViewItem.Tag as CodeObjectVM;
                if (codeObjectVM != null)
                    codeObjectVM.UpdateAnnotations();
            }
        }

        /// <summary>
        /// Find a tree item by name using a binary search (assumes the items are sorted alphabetically).
        /// </summary>
        /// <param name="itemsControl">The items control to be searched.</param>
        /// <param name="name">The name to be found.</param>
        /// <param name="codeObject">The associated code object.</param>
        /// <param name="index">The index of the matched node, or the insert location of the missing node.</param>
        /// <returns>The TreeViewItem if found, otherwise null.</returns>
        protected static TreeViewItem FindTreeItem(ItemsControl itemsControl, string name, CodeObject codeObject, out int index)
        {
            index = 0;
            ItemCollection items = itemsControl.Items;
            int low = 0, high = items.Count - 1;

            // Special handling for folders vs files (all folders appear first)
            if (codeObject == null)
            {
                // If we're finding a folder, ignore any files at the end of the collection
                while (high >= low && ((TreeViewItem)items[high]).Tag is CodeObjectVM)
                    --high;
                if (high < low)
                {
                    index = low;
                    return null;
                }
            }
            else
            {
                // If we're finding a file, ignore any folders at the beginning of the collection
                while (low <= high && ((TreeViewItem)items[low]).Tag == null)
                    ++low;
                if (low > high)
                {
                    index = low;
                    return null;
                }
            }

            // Do a binary search for the item
            TreeViewItem found = null;
            while (high >= low)
            {
                index = low + (high - low) / 2;
                int result = string.Compare(name, (string)((TreeViewItem)items[index]).Header, true);
                if (result < 0)
                    high = index - 1;
                else if (result > 0)
                {
                    low = index + 1;
                    if (low > high)
                    {
                        index = low;
                        break;
                    }
                }
                else
                {
                    found = (TreeViewItem)items[index];
                    break;
                }
            }
            return found;
        }

        protected static TreeViewItem FindFileNameInTree(ItemCollection items, string fileName)
        {
            foreach (TreeViewItem child in items)
            {
                if (child.Tag is CodeUnitVM && ((CodeUnitVM)child.Tag).CodeUnit.FileName == fileName)
                    return child;
                if (child.HasItems)
                {
                    TreeViewItem found = FindFileNameInTree(child.Items, fileName);
                    if (found != null)
                        return found;
                }
            }
            return null;
        }

        protected static TreeViewItem FindCodeObjectInTree<T>(ItemCollection items, T codeObject) where T : CodeObject
        {
            foreach (TreeViewItem child in items)
            {
                if (child.Tag is CodeObjectVM && ((CodeObjectVM)child.Tag).CodeObject == codeObject)
                    return child;
                if (child.HasItems)
                {
                    TreeViewItem found = FindCodeObjectInTree(child.Items, codeObject);
                    if (found != null)
                        return found;
                }
            }
            return null;
        }

        protected static void RemoveTreeItem(ItemsControl itemControl, CodeObject codeObject)
        {
            ItemCollection items = itemControl.Items;
            for (int i = 0; i < items.Count; ++i)
            {
                object tag = ((TreeViewItem)items[i]).Tag;
                if (tag is CodeObjectVM && ((CodeObjectVM)tag).CodeObject == codeObject)
                {
                    items.RemoveAt(i);
                    break;
                }
            }
        }

        public static void SetTreeItemColorBasedOnMessages(TreeViewItem treeViewItem)
        {
            CodeObjectVM codeObjectVM = treeViewItem.Tag as CodeObjectVM;
            if (codeObjectVM != null)
            {
                treeViewItem.Foreground = codeObjectVM.GetMessageBrush(Brushes.Black, CodeObjectVM.RenderFlags.None);
                if (treeViewItem.Foreground != Brushes.Black)
                    treeViewItem.BringIntoView();
            }
        }

        protected void SelectTreeItem(TreeViewItem treeViewItem)
        {
            treeViewItem.IsSelected = true;
            treeViewItem.BringIntoView();
            treeViewFiles.UpdateLayout();
            treeViewItem.BringIntoView();
        }

        #region /* FILE TREE EVENTS */

        protected static void treeViewFiles_ToolTipOpening(object sender, ToolTipEventArgs e)
        {
            TreeViewItem item = sender as TreeViewItem;
            if (item != null)
            {
                // Generate tooltips dynamically for CodeObjectVMs in the solution tree
                if (item.Tag is CodeObjectVM)
                {
                    StackPanel panel = new StackPanel();
                    CodeRenderer renderer = new CodeRenderer(panel);
                    CodeObjectVM codeObjectVM = (CodeObjectVM)item.Tag;
                    // Create a new VM to render the tooltip, because the height/width will differ for a description
                    CodeObjectVM.CreateVM(codeObjectVM.CodeObject, codeObjectVM.ParentVM, true).RenderToolTip(renderer);
                    item.ToolTip = panel;
                }
            }
        }

        protected void treeViewFiles_ContextMenuOpening(object sender, ContextMenuEventArgs e)
        {
            ContextMenu contextMenu = null;
            TreeViewItem item = (TreeViewItem)treeViewFiles.SelectedItem;
            if (item != null)
            {
                object tag = item.Tag;
                contextMenu = new ContextMenu();

                // Add general menu items
                bool isRenderable = (tag is CodeObjectVM && ((CodeObjectVM)tag).CodeObject.IsRenderable);
                bool hasChildren = item.HasItems;
                if (hasChildren || isRenderable)
                {
                    WPFUtil.AddMenuItem(contextMenu, "Open", miOpen_Click, !_busy);
                    WPFUtil.AddMenuItem(contextMenu, "Save", miSave_Click, !_busy);
                    WPFUtil.AddMenuItem(contextMenu, "Diff", miDiff_Click, !_busy);
                }
                if (hasChildren)
                {
                    if (contextMenu.Items.Count > 0)
                        WPFUtil.AddSeparator(contextMenu);
                    WPFUtil.AddMenuItem(contextMenu, "Expand All", miExpandAll_Click, !_busy);
                    WPFUtil.AddMenuItem(contextMenu, "Collapse All", miCollapseAll_Click, !_busy);
                }

                if (contextMenu.Items.Count == 0)
                    contextMenu = null;
                else
                    contextMenu.IsOpen = true;
            }
            ((FrameworkElement)sender).ContextMenu = contextMenu;
            e.Handled = true;
        }

        protected void treeViewFiles_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
        {
            // Give focus to an item if right-clicked
            TreeViewItem item = sender as TreeViewItem;
            if (item != null)
            {
                item.Focus();
                e.Handled = true;
            }
        }

        protected static void ExecuteActionOnTree(TreeViewItem treeViewItem, Action<TreeViewItem, object, Action<TreeViewItem>> action, object parameter,
            string actionName, Action<TreeViewItem, object> complete, object completeParameter)
        {
            if (treeViewItem == null)
                return;
            TreeAction treeAction = new TreeAction(treeViewItem, action, parameter, actionName, TreeActionComplete, complete, completeParameter);
            treeAction.Start();
        }

        protected static void TreeActionComplete(TreeViewItem treeViewItem, object parameter1, object parameter2)
        {
            if (parameter1 != null)
                ((Action<TreeViewItem, object>)parameter1)(treeViewItem, parameter2);
        }

        protected TabItem OpenItem(TreeViewItem treeViewItem)
        {
            TabItem tabItem = null;
            if (treeViewItem != null)
            {
                object obj = treeViewItem.Tag;
                if (obj is CodeObjectVM)
                {
                    CodeObjectVM codeObjectVM = (CodeObjectVM)obj;
                    if (codeObjectVM.CodeObject.IsRenderable)
                        tabItem = AddTab((string)treeViewItem.Header, codeObjectVM, treeViewItem);
                }
            }
            return tabItem;
        }

        protected void OpenItemAction(TreeViewItem treeViewItem, object parameter, Action<TreeViewItem> completed)
        {
            OpenItem(treeViewItem);
            completed(treeViewItem);
        }

        protected void miOpen_Click(object sender, RoutedEventArgs e)
        {
            ExecuteActionOnTree(treeViewFiles.SelectedItem as TreeViewItem, OpenItemAction, null, null, null, null);
        }

        protected void treeViewFiles_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
            // Double-clicking on a file opens it
            OpenItem(treeViewFiles.SelectedItem as TreeViewItem);
        }

        protected void ParseItemAction(TreeViewItem treeViewItem, object parameter, Action<TreeViewItem> completed)
        {
            // Only parse CodeUnits here
            object obj = treeViewItem.Tag;
            if (obj is CodeUnitVM)
                WPFUtil.ExecuteActionOnThread(this, ParseItemThread, ((CodeUnitVM)obj).CodeUnit, completed, treeViewItem);
        }

        private static void ParseItemThread(CodeUnit codeUnit)
        {
            codeUnit.Parse();
        }

        protected void ParseComplete(TreeViewItem treeViewItem, object parameter)
        {
            if (Unrecognized.Count > 0)
                Log.WriteLine("UNRECOGNIZED OBJECT COUNT: " + Unrecognized.Count);
            ShowProgressBar(null);
            if (parameter != null)
                ((Action<TreeViewItem>)parameter)(treeViewItem);
            else
                Busy = false;
        }

        public void ParseItem(CodeUnit codeUnit)
        {
            Busy = true;
            ShowProgressBar("Parsing");
            Unrecognized.Count = 0;
            codeUnit.Parse();
            ParseComplete(null, null);
        }

        protected void SaveItemAction(TreeViewItem treeViewItem, object parameter, Action<TreeViewItem> completed)
        {
            object obj = treeViewItem.Tag;
            if (obj is CodeObjectVM)
            {
                CodeObject codeObject = ((CodeObjectVM)obj).CodeObject;
                if (codeObject is CodeUnit)
                    ((CodeUnit)codeObject).Save();
            }
            completed(treeViewItem);
        }

        protected void miSave_Click(object sender, RoutedEventArgs e)
        {
            Busy = true;
            ShowProgressBar("Saving");
            ExecuteActionOnTree(treeViewFiles.SelectedItem as TreeViewItem, SaveItemAction, null, "Saved", SaveComplete, null);
        }

        protected void SaveComplete(TreeViewItem treeViewItem, object parameter)
        {
            ShowProgressBar(null);
            Busy = false;
        }

        protected void DiffItemAction(TreeViewItem treeViewItem, object parameter, Action<TreeViewItem> completed)
        {
            object obj = treeViewItem.Tag;
            if (obj is CodeObjectVM)
            {
                CodeObject codeObject = ((CodeObjectVM)obj).CodeObject;
                // Process files
                if (codeObject is CodeUnit)
                {
                    string diffProgram = System.Configuration.ConfigurationManager.AppSettings["DiffProgram"];
                    if (!string.IsNullOrEmpty(diffProgram))
                    {
                        // Save the file into a TEMP file and diff with the original
                        CodeUnit file = (CodeUnit)codeObject;
                        string tempFile = Path.GetTempFileName();
                        file.SaveAs(tempFile);
                        Process.Start(diffProgram, "\"" + file.FileName + "\" \"" + tempFile + "\"");
                    }
                }
            }
            completed(treeViewItem);
        }

        protected void miDiff_Click(object sender, RoutedEventArgs e)
        {
            Busy = true;
            ExecuteActionOnTree(treeViewFiles.SelectedItem as TreeViewItem, DiffItemAction, null, null, DiffComplete, null);
        }

        protected void DiffComplete(TreeViewItem treeViewItem, object parameter)
        {
            ShowProgressBar(null);
            Busy = false;
        }

        public static string GetDescriptionAttributeValue(MemberInfo thisMemberInfo)
        {
            DescriptionAttribute[] attributes = (DescriptionAttribute[])thisMemberInfo.GetCustomAttributes(typeof(DescriptionAttribute), false);
            return (attributes.Length > 0 ? attributes[0].Description : null);
        }

        protected void miExpandAll_Click(object sender, RoutedEventArgs e)
        {
            ExpandCollapseAll(true);
        }

        protected void miCollapseAll_Click(object sender, RoutedEventArgs e)
        {
            ExpandCollapseAll(false);
        }

        protected void ExpandCollapseAll(bool expand)
        {
            TreeViewItem item = treeViewFiles.SelectedItem as TreeViewItem;
            if (item != null && item.HasItems)
                ExpandCollapseAll(item, expand);
        }

        protected void ExpandCollapseAll(TreeViewItem treeViewItem, bool expand)
        {
            treeViewItem.IsExpanded = expand;
            foreach (TreeViewItem childItem in treeViewItem.Items)
            {
                if (childItem.HasItems)
                    ExpandCollapseAll(childItem, expand);
            }
        }

        #endregion

        #endregion

        #region /* FONT SELECTION */

        protected void comboBoxFonts_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            string fontName = comboBoxFonts.SelectedItem.ToString();
            if (CodeRenderer.CodeFontFamily.Source != fontName)
            {
                CodeRenderer.CodeFontFamily = new FontFamily(comboBoxFonts.SelectedItem.ToString());
                ReRenderCurrentTab();
            }
        }

        protected void textBoxFontSize_TextChanged(object sender, TextChangedEventArgs e)
        {
            double fontSize = StringUtil.ParseDouble(textBoxFontSize.Text);
            if (CodeRenderer.CodeFontSize != fontSize)
            {
                CodeRenderer.CodeFontSize = fontSize;
                ReRenderCurrentTab();
            }
        }

        #endregion

        #region /* OUTPUT WINDOWS */

        protected void AddLogEntry(string message, string toolTip)
        {
            if (listViewOutput.Dispatcher.CheckAccess())
                AddLogEntryLocal(message, toolTip);
            else
                listViewOutput.Dispatcher.Invoke(new Action<string, string>(AddLogEntryLocal), message, toolTip);
        }

        protected void AddLogEntryLocal(string message, string toolTip)
        {
            // Add the log entry to the list view
            ListViewItem entry = new ListViewItem { Content = message, ToolTip = toolTip };
            //entry.new String[] { DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.ffff"), message });

            listViewOutput.Items.Add(entry);
            while (listViewOutput.Items.Count > 9999)
                listViewOutput.Items.RemoveAt(0);

            //if (!menuViewFreeze.Checked)
            WPFUtil.ShowLastItem(listViewOutput);
        }

        protected void tabControlOutput_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (e.Source == tabControlOutput)
            {
                // Select the last line in the Output window if its tab was brought to the front
                if (tabItemOutput.IsSelected && listViewOutput.Items.Count > 0)
                    WPFUtil.ShowLastItem(listViewOutput);
            }
        }

        protected void SelectCodeObject(CodeUnit codeUnit, CodeObject codeObject)
        {
            // Open the CodeUnit (or find the tab if already open).  Note that the same physical file might be included
            // in more than one project, in which case different CodeUnit objects will exist so that different compiler
            // directives can product different parse trees.
            TabItem tabItem = OpenCodeUnit(codeUnit);

            // Select the UI object corresponding to the code object
            if (tabItem != null)
                ((CodeWindow)tabItem.Content).SelectCodeObject(codeObject);
        }

        /// <summary>
        /// Select the specified <see cref="CodeObject"/>, opening the containing <see cref="CodeUnit"/> first if necessary.
        /// </summary>
        /// <param name="codeObject"></param>
        public void SelectCodeObject(CodeObject codeObject)
        {
            if (codeObject != null)
            {
                CodeUnit codeUnit = codeObject.FindParent<CodeUnit>();
                if (codeUnit != null)
                    SelectCodeObject(codeUnit, codeObject);
            }
        }

        #endregion

        #region /* STATUS BAR */

        protected void ShowProgressBar(string status)
        {
            textBlockStatus.Text = status;
            progressBarStatus.Visibility = (status != null ? Visibility.Visible : Visibility.Hidden);
        }

        protected void UpdateHeapUsage(int heapUsage)
        {
            btnHeapSize.Content = heapUsage + " MB";
            _heapUsage = heapUsage;
            if (heapUsage > _maxHeapUsage)
            {
                _maxHeapUsage = heapUsage;
                btnHeapSize.ToolTip = "Max: " + _maxHeapUsage;
            }
        }

        private static int LowerFrequency;

        protected void OnUpdateStatus(object sender, EventArgs e)
        {
            // Update the heap size
            if (++LowerFrequency >= 4)
            {
                btnHeapSize_Click(null, null);
                LowerFrequency = 0;
            }
        }

        protected void btnHeapSize_Click(object sender, RoutedEventArgs e)
        {
            int heapUsage = (int)(GC.GetTotalMemory(false) / (1024 * 1024));
            if (heapUsage != _heapUsage)
                UpdateHeapUsage(heapUsage);
        }

        #endregion

        #region /* MAINWINDOW EVENTS */

        private void Window_MouseMove(object sender, MouseEventArgs e)
        {
            ResetTabToolTipTimer();
        }

        private void Window_Deactivated(object sender, EventArgs e)
        {
            CloseTabToolTip();
        }

        #endregion

        #region /* COMMANDS */

        public static readonly RoutedCommand CloseAllCommand = new RoutedCommand("Close All", typeof(MainWindow));
        public static readonly RoutedCommand SaveAllCommand = new RoutedCommand("Save All", typeof(MainWindow));
        public static readonly RoutedCommand AboutCommand = new RoutedCommand("About", typeof(MainWindow));

        public void BindCommands(Window window)
        {
            WPFUtil.AddCommandBinding(window, CloseAllCommand, OnCloseAll, CanCloseAllExecute);
            WPFUtil.AddCommandBinding(window, SaveAllCommand, OnFileSaveAll, CanSaveAllExecute);
            WPFUtil.AddCommandBinding(window, AboutCommand, OnAbout, CanAlwaysExecute);
        }

        #endregion
    }

    #region /* TREE ACTION CLASS */

    /// <summary>
    /// This class is used to execute an action on a tree of objects (such as files).
    /// </summary>
    public class TreeAction
    {
        public TreeViewItem StartTreeItem;
        public Action<TreeViewItem, object, Action<TreeViewItem>> Action;
        public object ActionParameter;
        public string DoneDescription;
        public Action<TreeViewItem, object, object> Complete;
        public object CompleteParameter1;
        public object CompleteParameter2;

        protected int _busyCount;
        protected Stopwatch _stopWatch;

        public TreeAction(TreeViewItem startTreeItem, Action<TreeViewItem, object, Action<TreeViewItem>> action, object actionParameter,
            string doneDescription, Action<TreeViewItem, object, object> complete, object completeParameter1, object completeParameter2)
        {
            StartTreeItem = startTreeItem;
            Action = action;
            ActionParameter = actionParameter;
            DoneDescription = doneDescription;
            Complete = complete;
            CompleteParameter1 = completeParameter1;
            CompleteParameter2 = completeParameter2;
        }

        public void Start()
        {
            _stopWatch = new Stopwatch();
            _stopWatch.Start();
            PerformActionOnTree(StartTreeItem);
        }

        protected void PerformActionOnTree(TreeViewItem treeViewItem)
        {
            if (treeViewItem != null)
            {
                object obj = treeViewItem.Tag;

                // Queue the action for (same thread) execution when the UI is idle
                if (obj is CodeObjectVM)
                {
                    ++_busyCount;
                    treeViewItem.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action<TreeViewItem>(PerformAction), treeViewItem);
                }

                // Process children items
                if (treeViewItem.HasItems)
                {
                    foreach (TreeViewItem childItem in treeViewItem.Items)
                        PerformActionOnTree(childItem);
                }
            }
        }

        protected void PerformAction(TreeViewItem treeViewItem)
        {
            // Perform the action on the tree item
            Action(treeViewItem, ActionParameter, ActionCompleted);
        }

        private void ActionCompleted(TreeViewItem treeViewItem)
        {
            MainWindow.SetTreeItemColorBasedOnMessages(treeViewItem);

            if (_busyCount > 0)
            {
                // Decrement the busy count
                --_busyCount;

                // Check if we're done
                if (_busyCount == 0)
                {
                    // If a description was provided, then log the results
                    if (DoneDescription != null)
                    {
                        Log.WriteLine(DoneDescription + " '" + StartTreeItem.Header + "', elapsed time: " + _stopWatch.Elapsed.TotalSeconds.ToString("N3"));
                        _stopWatch.Reset();
                    }

                    // Call the completion routine
                    if (Complete != null)
                        Complete(StartTreeItem, CompleteParameter1, CompleteParameter2);
                }
            }
        }
    }

    #endregion
}
